﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Xml.Linq;

using IrrlichtLime;
using IrrlichtLime.Core;
using IrrlichtLime.Scene;
using IrrlichtLime.Video;

namespace BillardPHS4700 {
    
    enum TablePieceType {
        WhiteBallRoot,
        WhiteBallVisual,
        Ball1,
        Ball2,
        Ball3,
        Ball4,
        Ball5,
        Ball6,
        Ball7,
        Ball8,
        Ball9,
        Ball10,
        Ball11,
        Ball12,
        Ball13,
        Ball14,
        Ball15,
        Carpet,
        InteriorCornerWithHole,
        MiddleWall,
        MiddleWallWithHole,
        Stick,
        Triangle,
    };

    enum CameraType {
        TopDown,
        Side
    }

    /// <summary>
    /// Manages the basic Irrlicht objects: graphics device, scene manager, etc.
    /// </summary>
    class Engine {

        IrrlichtDevice m_device;
        VideoDriver m_driver;
        SceneManager m_sceneManager;
        Dictionary<TablePieceType, string> m_meshPaths;
        Dictionary<CameraType, ICamera> m_cameras;
        CameraType m_activeCamera;
        string m_assetsRoot;

        /// <summary>
        /// Visual node of the ball; this one must always be at Position = 0,0,0 and can be rotated
        /// </summary>
        public SceneNode WhiteBallVisual { get; private set; }
        /// <summary>
        /// Root node of the ball; controls its position and that of the cue stick. Don't rotate this one!
        /// </summary>
        public SceneNode WhiteBallRoot { get; private set; }


        /// <summary>
        /// Balls 1-15
        /// </summary>
        public List<SceneNode> Balls { get; private set; }
        /// <summary>
        /// Shadows for each ball (0 = white ball, 1-15 = other balls)
        /// </summary>
        public List<SceneNode> Shadows { get; private set; }
        /// <summary>
        /// Pieces that form the table
        /// </summary>
        public List<SceneNode> TablePieces { get; private set; }
        /// <summary>
        /// The cue stick node
        /// </summary>
        public SceneNode CueStick { get; private set; }

        public SceneCollisionManager CollisionManager {
            get {
                return m_sceneManager.SceneCollisionManager;
            }
        }

        /// <summary>
        /// Sets up the graphics device and initializes basic engine objects
        /// </summary>
        public void Initialize() {
            var parameters = new IrrlichtCreationParameters {
                DeviceType = DeviceType.Best,
                DriverType = DriverType.OpenGL,
                WindowSize = new Dimension2Di(1200, 700),
                VSync = true
            };
            m_cameras = new Dictionary<CameraType, ICamera>();
            m_device = IrrlichtDevice.CreateDevice(parameters);
            m_driver = m_device.VideoDriver;
            m_sceneManager = m_device.SceneManager;
            m_assetsRoot = Path.Combine(Environment.CurrentDirectory, "../assets");
            m_meshPaths = new Dictionary<TablePieceType, string> {
                { TablePieceType.WhiteBallVisual, "boule.obj" },
                { TablePieceType.Ball1, "boule1.obj" },
                { TablePieceType.Ball2, "boule2.obj" },
                { TablePieceType.Ball3, "boule3.obj" },
                { TablePieceType.Ball4, "boule4.obj" },
                { TablePieceType.Ball5, "boule5.obj" },
                { TablePieceType.Ball6, "boule6.obj" },
                { TablePieceType.Ball7, "boule7.obj" },
                { TablePieceType.Ball8, "boule8.obj" },
                { TablePieceType.Ball9, "boule9.obj" },
                { TablePieceType.Ball10, "boule10.obj" },
                { TablePieceType.Ball11, "boule11.obj" },
                { TablePieceType.Ball12, "boule12.obj" },
                { TablePieceType.Ball13, "boule13.obj" },
                { TablePieceType.Ball14, "boule14.obj" },
                { TablePieceType.Ball15, "boule15.obj" },
                { TablePieceType.Stick, "queue.obj" },
                { TablePieceType.Triangle, "triangle.obj" },
                { TablePieceType.MiddleWall, "mur.obj" },
                { TablePieceType.MiddleWallWithHole, "murTrou.obj" },
                { TablePieceType.InteriorCornerWithHole, "murCoinTrou.obj" },
                { TablePieceType.Carpet, "tapis.obj" }
            };
            InitRenderTree();
            InitCameras();
            m_device.OnEvent += OnDeviceEvent;
        }

        void InitCameras() {
            m_cameras[CameraType.TopDown] = new TopDownCamera();
            m_cameras[CameraType.TopDown].Initialize(m_sceneManager.AddCameraSceneNode());
            m_cameras[CameraType.Side] = new SideCamera(WhiteBallRoot);
            m_cameras[CameraType.Side].Initialize(m_sceneManager.AddCameraSceneNode());
            ActiveCamera = CameraType.TopDown;
        }

        bool OnDeviceEvent(Event evnt) {
            if (OnEvent != null) {
                OnEvent(evnt);
            }
            return false;
        }

        /// <summary>
        /// Draws the scene
        /// </summary>
        public void Draw() {
            m_driver.BeginScene();
            m_sceneManager.DrawAll();
            m_driver.EndScene();
        }

        public bool Update() {
            m_cameras[ActiveCamera].Update();
            return m_device.Run();
        }

        /// <summary>
        /// Gets or sets the active camera
        /// </summary>
        public CameraType ActiveCamera {
            get { return m_activeCamera; }
            set {
                foreach (var camera in m_cameras.Values) {
                    OnEvent -= camera.HandleEvent;
                }
                m_sceneManager.ActiveCamera = m_cameras[value].Node;
                OnEvent += m_cameras[value].HandleEvent;
                m_activeCamera = value;
            }
        }

        public void ResetCueStick() {
            CueStick.Position = new Vector3Df(0, -(5 + Constants.BallRadius), 0);
        }

        public event IrrlichtDevice.EventHandler OnEvent;

        /// <summary>
        /// Creates all the objects in the render tree
        /// </summary>
        void InitRenderTree() {
            // Initialize table
            var xmlFile = XDocument.Load(Path.Combine(m_assetsRoot, "mapsDeJeu/configuration.xml"));
            var tableElements = from d in xmlFile.Descendants("TableElement")
                                select new {
                                    PositionX = int.Parse(d.Attribute("PositionX").Value),
                                    PositionY = int.Parse(d.Attribute("PositionY").Value),
                                    Rotation = int.Parse(d.Attribute("Rotation").Value),
                                    Type = Utils.ParseEnum<TablePieceType>(d.Attribute("Type").Value)
                                 };

            TablePieces = new List<SceneNode>();
            foreach (var elem in tableElements) {
                // Get mesh and create node
                var type = elem.Type;
                var tableNode = AddAnimatedMeshSceneNode(type);
                tableNode.Scale = new Vector3Df(10.0f / 15.0f);
                TablePieces.Add(tableNode);

                // Set material properties
                tableNode.SetMaterialFlag(MaterialFlag.Lighting, true);

                // Put in its proper position and orientation
                tableNode.Position = new Vector3Df(elem.PositionX, elem.PositionY, Constants.TableHeight - 2.5f);
                tableNode.Rotation = new Vector3Df(0, 0, elem.Rotation);
            }

            var ballPositions = new Dictionary<TablePieceType, Vector2Di> {
                { TablePieceType.WhiteBallVisual, new Vector2Di(0, 0) },
                { TablePieceType.Ball1, new Vector2Di(0, 4) },
                { TablePieceType.Ball2, new Vector2Di(-1, 2) },
                { TablePieceType.Ball3, new Vector2Di(1, 2) },
                { TablePieceType.Ball4, new Vector2Di(0, 0) },
                { TablePieceType.Ball5, new Vector2Di(-2, 0) },
                { TablePieceType.Ball6, new Vector2Di(2, 0) },
                { TablePieceType.Ball7, new Vector2Di(-1, -2) },
                { TablePieceType.Ball8, new Vector2Di(-3, -2) },
                { TablePieceType.Ball9, new Vector2Di(1, -2) },
                { TablePieceType.Ball10, new Vector2Di(3, -2) },
                { TablePieceType.Ball11, new Vector2Di(0, -4) },
                { TablePieceType.Ball12, new Vector2Di(2, -4) },
                { TablePieceType.Ball13, new Vector2Di(4, -4) },
                { TablePieceType.Ball14, new Vector2Di(-2, -4) },
                { TablePieceType.Ball15, new Vector2Di(-4, -4) },
            };
            var ballPositionOrigin = new Vector3Df(0, 0, Constants.TableHeight + Constants.BallRadius);
            // Create white ball
            WhiteBallRoot = m_sceneManager.AddEmptySceneNode();
            WhiteBallRoot.Position = new Vector3Df(20, 0, Constants.TableHeight + Constants.BallRadius);

            WhiteBallVisual = AddAnimatedMeshSceneNode(TablePieceType.WhiteBallVisual, WhiteBallRoot);
            WhiteBallVisual.ID = 15;
            // Set material properties
            WhiteBallVisual.SetMaterialFlag(MaterialFlag.Lighting, true);
            var material = WhiteBallVisual.GetMaterial(0);
            material.Shininess = 90f;
            material.SpecularColor.Set(255, 255, 255, 255);
            WhiteBallVisual.Scale = new Vector3Df(Constants.BallRadius / 5.25f);

            Balls = new List<SceneNode>();
            // Create balls 1 to 15
            for (var i = (int)TablePieceType.Ball1; i <= (int)TablePieceType.Ball15; ++i) {
                // Get mesh and create node
                var type = (TablePieceType)i;
                var ball = CreateBall(type, ballPositionOrigin, ballPositions);
                ball.ID = i - (int)TablePieceType.Ball1;
                Balls.Add(ball);
            }

            // Load queue stick
            CueStick = AddAnimatedMeshSceneNode(TablePieceType.Stick, WhiteBallRoot);
            CueStick.Scale = new Vector3Df(0.7f);
            ResetCueStick();
            CueStick.SetMaterialFlag(MaterialFlag.Lighting, true);

            // Load shadows
            Shadows = new List<SceneNode>();
            AddShadows();

            // Add light to the scene
            m_sceneManager.AmbientLight = new Colorf(0.5f, 0.5f, 0.5f);
            var pointLight = m_sceneManager.AddLightSceneNode(null, new Vector3Df(10f, 100f, Constants.LightHeight), new Colorf(1f, 1f, 1f), 100f);
            pointLight.LightType = LightType.Point;
        }

        void AddShadows() {
            var mesh = m_sceneManager.GeometryCreator.CreatePlaneMesh(new Dimension2Df(2 * Constants.BallRadius));
            var texture = m_driver.GetTexture(Path.Combine(m_assetsRoot, "Shadow.png"));
            AddShadowNode(mesh, texture, WhiteBallRoot);
            foreach(var ball in Balls) {
                AddShadowNode(mesh, texture, ball);
            }
        }

        void AddShadowNode(Mesh mesh, Texture texture, SceneNode ball) {
            var node = m_sceneManager.AddMeshSceneNode(mesh);
            node.SetMaterialTexture(0, texture);
            node.SetMaterialType(MaterialType.TransparentAlphaChannel);
            node.Position = ball.Position - new Vector3Df(0, 0, Constants.BallRadius - 0.2f);
            node.Rotation = new Vector3Df(90, 0, 0);
            Shadows.Add(node);
        }

        AnimatedMeshSceneNode AddAnimatedMeshSceneNode(TablePieceType type, SceneNode parent = null) {
            var meshPath = GetMeshPath(type);
            var mesh = m_sceneManager.GetMesh(meshPath);
            var node = m_sceneManager.AddAnimatedMeshSceneNode(mesh, parent);
            return node;
        }

        AnimatedMeshSceneNode CreateBall(TablePieceType type, Vector3Df ballPositionOrigin, Dictionary<TablePieceType, Vector2Di> ballPositions) {
            var meshPath = GetMeshPath(type);
            var mesh = m_sceneManager.GetMesh(meshPath);
            var node = m_sceneManager.AddAnimatedMeshSceneNode(mesh);
            node.Scale = new Vector3Df(Constants.BallRadius / 5.25f); //5.25 is the radius of the actual models

            // Set material properties
            node.SetMaterialFlag(MaterialFlag.Lighting, true);
            var material = node.GetMaterial(0);
            material.Shininess = 90f;
            material.SpecularColor.Set(255, 255, 255, 255);

            // Put in its proper position
            node.Position = new Vector3Df(
                ballPositionOrigin.X + (ballPositions[type].X * Constants.BallRadius),
                ballPositionOrigin.Y + (ballPositions[type].Y * Constants.BallRadius),
                ballPositionOrigin.Z);

            return node;
        }

        /// <summary>
        /// Gets the path of the mesh of the specified piece type
        /// </summary>
        string GetMeshPath(TablePieceType tablePieceType) {
            return Path.Combine(m_assetsRoot, m_meshPaths[tablePieceType]);
        }
    }
}
